package com.streambolics.servlet;

import java.util.ArrayList;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.streambolics.std.AnchorBuilder;
import com.streambolics.std.HtmlBuilder;
import com.streambolics.std.PipedAnchorList;
import com.streambolics.std.StringExtensions;

/**
 * An HTTP servlet that produces different output depending on the client
 * request. <br/>
 * The ClientAwareServlet implements a lightweight MVC pattern to produce the
 * desired effect by breaking the production of a result in three different
 * steps:
 * <ul>
 * <li>
 * The first step is to retrieve the model object. The model object can be
 * loaded from the database, or created from scratch. This step is performed in
 * the generateResponseObject() method.</li>
 * <li>
 * The second step is to apply changes to the model object according to the
 * request object. This step is performed in the updateResponseObject() method.</li>
 * <li>
 * The third step is to produce output. This step is performed in the
 * formatResponseObject() method.</li>
 * </ul>
 * <br/>
 * The typical way of implementing the MVC pattern is to have a hierarchy of
 * classes:
 * <ul>
 * <li>
 * First, create a class for the model object. This class will be called Model
 * in the explanation.</li>
 * <li>
 * Create a class derived from ClientAwareServlet&lt;Model&gt; named
 * ModelHandler.</li>
 * <li>
 * In ModelHandler, implement the formatting of the model object. Typically this
 * is not done by redefining formatResponseObject(), but redefining the
 * formatXxxAsYyy () methods instead.</li>
 * <li>
 * In ModelHandler, implement generateResponseObject() to load the model object
 * from the database. This is typically done by looking up the request
 * parameters for an parameter named "ID", and using this as the internal Id of
 * the object to load.</li>
 * <li>
 * Create classes derived from ModelHandler to handle each possible command. The
 * class is typically named ModelCommandHandler.</li>
 * <li>
 * In ModelCommandHandler classes, redefine the updateResponseObject() method to
 * apply any change requested by the query.</li>
 * <li>
 * In some cases, the command does not act on an existing model object, but
 * creates one from scratch. In this case, redefine the generateResponseObject()
 * method also, to create the object, and link it to the database. It is best,
 * in that case, to also redefine getRedirect() to point to the base class. This
 * makes sure that after the query has been processed, the same query is not
 * re-done when the user clicks the Refresh button of the browser.</li>
 * </ul>
 * 
 * @author Sauron
 * 
 */

public abstract class ClientAwareServlet<T> extends StandardServlet
{
    private static final long    serialVersionUID = -5830660249868636413L;

    ServletClientInfo            _clientInfo;

    private ArrayList<Exception> _warnings;

    /**
     * Fill the bottom menu items.
     * <p>
     * A derived class should redefine this to add anchors in the menu at the
     * bottom of the HTML document. Menu items at the bottom should be specific
     * to the currently displayed object.
     * </p>
     * 
     * @param aBuilder
     *            The builder where the anchors shoud be added.
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions.
     */

    protected void addBottomMenuItems (AnchorBuilder aBuilder, T aResponseObject) throws Exception
    {
    }

    /**
     * Fill the top menu items.
     * <p>
     * A derived class should redefine this to add anchors in the menu at the
     * top of the HTML document. Menu items at the top should normally manage
     * global features.
     * </p>
     * 
     * @param aBuilder
     *            The builder where the anchors shoud be added.
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions.
     */

    protected void addTopMenuItems (AnchorBuilder aBuilder, T aResponseObject) throws Exception
    {
        aBuilder.addAnchor ("http://code.google.com/p/worseminton/issues/list", "Report issues");
    }

    protected ServletClientInfo buildClientInfo (HttpServletRequest aRequest)
    {
        return ServletClientInfo.getFor (aRequest);
    }

    @Override
    protected final void buildResponse (HttpServletRequest aRequest, HttpServletResponse aResponse) throws Exception
    {
        T r = generateResponseObject (aRequest);
        updateResponseObject (aRequest, r);
        formatResponseObject (aResponse, r);
    }

    /**
     * Generates the HTML code for the body of the document as shown on a
     * desktop computer.
     * <p>
     * This should be redefined by a derived view class.
     * </p>
     * 
     * @param aBuilder
     *            The builder where HTML code must be appended
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    protected void formatBodyAsSimpleDesktopHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
    }

    /**
     * Generates the HTML code for the body of the document as shown on a mobile
     * device.
     * <p>
     * This should be redefined by a derived view class.
     * </p>
     * 
     * @param aBuilder
     *            The builder where HTML code must be appended
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    protected void formatBodyAsSimpleMobileHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
    }

    /**
     * Generates the HTML code for the bottom (footer) of the document when
     * rendered on a desktop computer.
     * <p>
     * The default behaviour is to call addBottomMenuItems() to populate a
     * contextual menu.
     * </p>
     * 
     * @param aBuilder
     *            The builder where HTML code must be appended
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    protected void formatBottomAsSimpleDesktopHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
        aBuilder.openDiv ("bottom");
        AnchorBuilder b = new PipedAnchorList (aBuilder);
        addBottomMenuItems (b, aResponseObject);
        aBuilder.closeDiv ();
    }

    /**
     * Generates the HTML code for the bottom (footer) of the document when
     * rendered on a mobile device.
     * 
     * @param aBuilder
     *            The builder where HTML code must be appended
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    protected void formatBottomAsSimpleMobileHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
    }

    /**
     * Generates the HEAD elements for rendering on a desktop;
     * 
     * @param aBuilder
     *            The builder where HTML code must be appended
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    private void formatHeadAsSimpleDesktopHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
        aBuilder.setTitle (getTitleString (aResponseObject));
        aBuilder.addStyleSheet ("/css/desktop.css");
    }

    /**
     * Generates the HEAD elements for rendering on a mobile device.
     * 
     * @param aBuilder
     *            The builder where HTML code must be appended
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    private void formatHeadAsSimpleMobileHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
        aBuilder.setTitle (getTitleString (aResponseObject));
        aBuilder.addStyleSheet ("/css/mobile.css");
        aBuilder.addMetaName ("viewport", "height=device-height, width=device-width");
    }

    /**
     * Formats the response object into the servlet response.
     * <p>
     * 
     * @param aResponse
     *            The HTTP response object expecting the output.
     * @param aResponseObject
     *            The model object to format.
     * @throws Exception
     *             As per Streambolics Framework coding standards.
     */

    protected void formatResponseObject (HttpServletResponse aResponse, T aResponseObject) throws Exception
    {
        String s = null;
        if (_clientInfo.isUser ())
        {
            String redir = hasWarnings () ? null : getRedirect (aResponseObject);
            if (StringExtensions.isNotEmpty (redir))
            {
                redirectTo (redir);
            }
            else if (_clientInfo.isRia ())
            {
                if (_clientInfo.isMobile ())
                {
                    redirectTo ("/mobileapp.html");
                }
                else
                {
                    redirectTo ("/desktopapp.html");
                }
                return;
            }
            else
            {
                aResponse.setContentType ("text/html");
                HtmlBuilder b = new ServletHtmlBuilder (aResponse);
                if (_clientInfo.isMobile ())
                {
                    formatHeadAsSimpleMobileHtml (b, aResponseObject);
                    formatTopAsSimpleMobileHtml (b, aResponseObject);
                    formatBodyAsSimpleMobileHtml (b, aResponseObject);
                    formatWarningsAsSimpleMobileHtml (b, aResponseObject);
                    formatBottomAsSimpleMobileHtml (b, aResponseObject);
                }
                else
                {
                    formatHeadAsSimpleDesktopHtml (b, aResponseObject);
                    formatTopAsSimpleDesktopHtml (b, aResponseObject);
                    formatBodyAsSimpleDesktopHtml (b, aResponseObject);
                    formatWarningsAsSimpleDesktopHtml (b, aResponseObject);
                    formatBottomAsSimpleDesktopHtml (b, aResponseObject);
                }
                s = b.getHtml ();
            }
        }
        else
        {
            aResponse.setContentType ("text/plain");
        }
        if (s != null)
        {
            aResponse.getWriter ().println (s);
        }
        else
        {
            aResponse.getWriter ().println ("Nothing was generated");
            // aResponse.sendError (500,
            // "The application failed to produce a valid page");
        }
    }

    protected void formatTopAsSimpleDesktopHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
        aBuilder.openDiv ("top");
        AnchorBuilder b = new PipedAnchorList (aBuilder);
        addTopMenuItems (b, aResponseObject);
        aBuilder.closeDiv ();
    }

    protected void formatTopAsSimpleMobileHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
    }

    protected void formatWarningsAsSimpleDesktopHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
        formatWarningsAsSimpleMobileHtml (aBuilder, aResponseObject);
    }

    protected void formatWarningsAsSimpleMobileHtml (HtmlBuilder aBuilder, T aResponseObject) throws Exception
    {
        for (Exception e : _warnings)
        {
            aBuilder.openDiv ("error");
            aBuilder.addParagraph (e.getMessage ());
            aBuilder.closeDiv ();
        }
    }

    protected abstract T generateResponseObject (HttpServletRequest aRequest) throws Exception;

    /**
     * The ClientInfo for the current request.
     * 
     * @return The ClientInfo structure.
     */

    protected ServletClientInfo getClientInfo ()
    {
        return _clientInfo;
    }

    /**
     * The URL where the user should be redirected after processing.
     * <p>
     * If this function returns a non-empty string, the browser will be
     * redirected to the given URL, and the formatting functions will not be
     * called.
     * </p>
     * <p>
     * A derived class would typically use a redirection after an object has
     * been changed or create, so that the URL in the browser no longer contains
     * the update command. This is important if the user refreshes the page, as
     * the update command would be executed again.
     * </p>
     * <p>
     * Redirection is not attempted if there was an error reported by
     * warnAbout() during the creation or the update of the model object, since
     * in that case a retry would be the appropriate thing to do.
     * </p>
     * <p>
     * Redirection is not attempted if the rendering will generate something
     * else than HTML. In that case it is assumed that the client is not a plain
     * browser, but an application using the page as a web service or Ajax data
     * provider. It is assumed that the application will not abusively retry the
     * operation, and that a redirect may be difficult to handle in the client
     * application.
     * </p>
     * 
     * @param aResponseObject
     *            The model object.
     * @return The full URL to redirect to. This URL will be rewritten by the
     *         Session URL rewriter.
     */

    protected String getRedirect (T aResponseObject)
    {
        return null;
    }

    /**
     * The title to be used.
     * 
     * @param aResponseObject
     *            The model object.
     * @return The string to be used as title. The string will be escaped before
     *         being used.
     * @throws Exception
     *             As per Streambolics Core conventions.
     */

    protected String getTitleString (T aResponseObject) throws Exception
    {
        return aResponseObject.getClass ().getSimpleName ();
    }

    /**
     * Whether there were warnings reported by warnAbout().
     * 
     * @return True if at least one exception was reported by warnAbout(), false
     *         otherwise.
     */

    protected boolean hasWarnings ()
    {
        return _warnings.size () > 0;
    }

    /**
     * Fills the response header with a redirection.
     * 
     * @param aUrl
     *            The URL to go to. This URL will be rewritten by the Session
     *            URL rewriter.
     * @throws Exception
     *             As per Streambolics Core conventions.
     */

    protected void redirectTo (String aUrl) throws Exception
    {
        getResponse ().sendRedirect (getResponse ().encodeRedirectURL (aUrl));
    }

    @Override
    protected void setupEnvironment (HttpServletRequest aRequest, HttpServletResponse aResponse) throws Exception
    {
        super.setupEnvironment (aRequest, aResponse);
        _clientInfo = buildClientInfo (aRequest);
        _warnings = new ArrayList<Exception> ();
    }

    /**
     * Performs changes as requested by the client to the Model object.
     * 
     * @param aRequest
     *            The HTTP request
     * @param aResponseObject
     *            The model object
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    protected void updateResponseObject (HttpServletRequest aRequest, T aResponseObject) throws Exception
    {
    }

    /**
     * Warns the user about an error that occurred during processing.
     * <p>
     * A derived controller class will typically call this function to report
     * any problems encountered during processing.
     * </p>
     * <p>
     * The framework will report the error to the user in a gentle manner, and
     * not by shouting about a misbehaving application that needs to be shut
     * down.
     * </p>
     * 
     * @param e
     *            The exception describing the problem to report.
     */

    protected void warnAbout (Exception e)
    {
        _warnings.add (e);
    }
}
